/* Copyright (c) 2001 - 2013 OpenPlans - www.openplans.org. All rights reserved.
 * This code is licensed under the GPL 2.0 license, available at the root
 * application directory.
 */
package org.geoserver.catalog.impl;

import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;

import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.CatalogException;
import org.geoserver.catalog.CatalogFacade;
import org.geoserver.catalog.CatalogFactory;
import org.geoserver.catalog.CatalogInfo;
import org.geoserver.catalog.CatalogValidator;
import org.geoserver.catalog.CatalogVisitor;
import org.geoserver.catalog.CoverageDimensionInfo;
import org.geoserver.catalog.CoverageInfo;
import org.geoserver.catalog.CoverageStoreInfo;
import org.geoserver.catalog.DataStoreInfo;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.KeywordInfo;
import org.geoserver.catalog.LayerGroupHelper;
import org.geoserver.catalog.LayerGroupInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.catalog.MapInfo;
import org.geoserver.catalog.NamespaceInfo;
import org.geoserver.catalog.PublishedInfo;
import org.geoserver.catalog.ResourceInfo;
import org.geoserver.catalog.ResourcePool;
import org.geoserver.catalog.StoreInfo;
import org.geoserver.catalog.StyleInfo;
import org.geoserver.catalog.WMSLayerInfo;
import org.geoserver.catalog.WMSStoreInfo;
import org.geoserver.catalog.WorkspaceInfo;
import org.geoserver.catalog.event.CatalogAddEvent;
import org.geoserver.catalog.event.CatalogEvent;
import org.geoserver.catalog.event.CatalogListener;
import org.geoserver.catalog.event.CatalogModifyEvent;
import org.geoserver.catalog.event.CatalogPostModifyEvent;
import org.geoserver.catalog.event.CatalogRemoveEvent;
import org.geoserver.catalog.event.impl.CatalogAddEventImpl;
import org.geoserver.catalog.event.impl.CatalogModifyEventImpl;
import org.geoserver.catalog.event.impl.CatalogPostModifyEventImpl;
import org.geoserver.catalog.event.impl.CatalogRemoveEventImpl;
import org.geoserver.catalog.util.CloseableIterator;
import org.geoserver.ows.util.OwsUtils;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.GeoServerResourceLoader;
import org.geotools.util.logging.Logging;
import org.opengis.feature.type.Name;
import org.opengis.filter.Filter;
import org.opengis.filter.sort.SortBy;

/**
 * A default catalog implementation that is memory based.
 * 
 * @author Justin Deoliveira, The Open Planning Project
 * 
 * TODO: remove synchronized blocks, make setting of default workspace/namespace part
 *   of dao contract
 * TODO: move resolve() to dao 
 *   
 */
public class CatalogImpl implements Catalog {
    
    /**
     * logger
     */
    private static final Logger LOGGER = Logging.getLogger(CatalogImpl.class);

    /**
     * data access facade
     */
    protected CatalogFacade facade;
    
    /**
     * listeners
     */
    protected List listeners = new ArrayList();

    /** 
     * resources
     */
    protected ResourcePool resourcePool;
    protected GeoServerResourceLoader resourceLoader;

    /**
     * extended validation switch
     */
    protected boolean extendedValidation = true;

    public CatalogImpl() {
        facade = new DefaultCatalogFacade(this);
        resourcePool = ResourcePool.create(this);
    }
    
    public CatalogFacade getFacade() {
        return facade;
    }

    /**
     * Turn on/off extended validation switch.
     * <p>
     * This is not part of the public api, it is used for testing purposes where we have to 
     * bootstrap catalog contents. 
     * </p>
     */
    public void setExtendedValidation(boolean extendedValidation) {
        this.extendedValidation = extendedValidation;
    }

    public boolean isExtendedValidation() {
        return extendedValidation;
    }

    public Iterable<CatalogValidator> getValidators() {
        return GeoServerExtensions.extensions(CatalogValidator.class);
    }
    
    public void setFacade(CatalogFacade facade) {
        this.facade = facade;
        facade.setCatalog(this);
    }
    
    public String getId() {
        return "catalog";
    }
    
    public CatalogFactory getFactory() {
        return new CatalogFactoryImpl( this );
    }

    // Store methods
    public void add(StoreInfo store) {
        
        if ( store.getWorkspace() == null ) {
            store.setWorkspace( getDefaultWorkspace() );
        }

        validate(store, true);

        //TODO: remove synchronized block, need transactions
        StoreInfo added;
        synchronized (facade) {
            added = facade.add(resolve(store));
            
            // if there is no default store use this one as the default
            if(getDefaultDataStore(store.getWorkspace()) == null && store instanceof DataStoreInfo) {
                setDefaultDataStore(store.getWorkspace(), (DataStoreInfo) store);
            }
        }
        added(added);
    }

    public List<RuntimeException> validate(StoreInfo store, boolean isNew) {
        if ( isNull(store.getName()) ) {
            throw new IllegalArgumentException( "Store name must not be null");
        }
        if ( store.getWorkspace() == null ) {
            throw new IllegalArgumentException( "Store must be part of a workspace");
        }
        
        WorkspaceInfo workspace = store.getWorkspace();
        StoreInfo existing = getStoreByName( workspace, store.getName(), StoreInfo.class );
        if ( existing != null && (isNew || !existing.getId().equals( store.getId() )) ) {
            String msg = "Store '"+ store.getName() +"' already exists in workspace '"+workspace.getName()+"'";
            throw new IllegalArgumentException( msg );
        }    

        return postValidate(store, isNew);
    }
    
    public void remove(StoreInfo store) {
        if ( !getResourcesByStore(store, ResourceInfo.class).isEmpty() ) {
            throw new IllegalArgumentException( "Unable to delete non-empty store.");
        }
        
        //TODO: remove synchronized block, need transactions
        synchronized(facade) {
            facade.remove(store);
            
            WorkspaceInfo workspace = store.getWorkspace();
            DataStoreInfo defaultStore = getDefaultDataStore(workspace);
            if (store.equals(defaultStore) || defaultStore == null) {
                //TODO: this will fire multiple events, we want to fire only one
                setDefaultDataStore(workspace, null);
                
                // default removed, choose another store to become default if possible
                List dstores = getStoresByWorkspace(workspace, DataStoreInfo.class);
                if (!dstores.isEmpty()) {
                    setDefaultDataStore(workspace, (DataStoreInfo) dstores.get(0));
                }
            }
        }
        
        removed(store);
    }

    public void save(StoreInfo store) {
        if ( store.getId() == null ) {
            //add it instead of saving
            add( store );
            return;
        }
        validate(store, false);
        facade.save(store);
    }

    public <T extends StoreInfo> T detach(T store) {
        return detached(store, facade.detach(store));
    }

    public <T extends StoreInfo> T getStore(String id, Class<T> clazz) {
        return facade.getStore(id, clazz);
    }

    public <T extends StoreInfo> T getStoreByName(String name, Class<T> clazz) {
        return getStoreByName((WorkspaceInfo) null, name, clazz);
    }

    public <T extends StoreInfo> T getStoreByName(WorkspaceInfo workspace,
            String name, Class<T> clazz) {
        
        WorkspaceInfo ws = workspace;
        if (ws == null) {
            ws = getDefaultWorkspace();
        }
        
        if(DataStoreInfo.class == clazz && (name == null || name.equals(Catalog.DEFAULT))) {
            return (T) getDefaultDataStore(workspace);
        }
        
        T store = facade.getStoreByName(ws, name, clazz);
        if (store == null && workspace == null) {
            store = facade.getStoreByName(CatalogFacade.ANY_WORKSPACE, name, clazz);
        }
        return store;
    }

    public <T extends StoreInfo> T getStoreByName(String workspaceName,
           String name, Class<T> clazz) {
        if (workspaceName == null) {
            return getStoreByName((WorkspaceInfo)null, name, clazz);
        }
        
        WorkspaceInfo workspace = getWorkspaceByName(workspaceName);
        if (workspace != null) {
            return getStoreByName(workspace, name, clazz);
        }
        return null;
    }
    
    public <T extends StoreInfo> List<T> getStoresByWorkspace(
            String workspaceName, Class<T> clazz) {
        
        WorkspaceInfo workspace = null;
        if ( workspaceName != null ) {
            workspace = getWorkspaceByName(workspaceName);
            if ( workspace == null ) {
                return Collections.EMPTY_LIST;
            }
        }
        
        return getStoresByWorkspace(workspace, clazz);
        
    }

    public <T extends StoreInfo> List<T> getStoresByWorkspace(
            WorkspaceInfo workspace, Class<T> clazz) {

        return facade.getStoresByWorkspace(workspace, clazz);
    }

    public List getStores(Class clazz) {
        return facade.getStores(clazz);
    }

    public DataStoreInfo getDataStore(String id) {
        return (DataStoreInfo) getStore(id, DataStoreInfo.class);
    }

    public DataStoreInfo getDataStoreByName(String name) {
        return (DataStoreInfo) getStoreByName(name,DataStoreInfo.class);
    }

    public DataStoreInfo getDataStoreByName(String workspaceName, String name) {
        return (DataStoreInfo) getStoreByName(workspaceName, name, DataStoreInfo.class);
    }

    public DataStoreInfo getDataStoreByName(WorkspaceInfo workspace, String name) {
        return (DataStoreInfo) getStoreByName(workspace, name, DataStoreInfo.class);
    }

    public List<DataStoreInfo> getDataStoresByWorkspace(String workspaceName) {
        return getStoresByWorkspace( workspaceName, DataStoreInfo.class );
    }
    
    public List<DataStoreInfo> getDataStoresByWorkspace(WorkspaceInfo workspace) {
        return getStoresByWorkspace( workspace, DataStoreInfo.class );
    }

    public List getDataStores() {
        return getStores(DataStoreInfo.class);
    }
    
    public DataStoreInfo getDefaultDataStore(WorkspaceInfo workspace) {
        return facade.getDefaultDataStore(workspace);
    }
    
    public void setDefaultDataStore(WorkspaceInfo workspace, DataStoreInfo store) {
        if (store != null) {
            // basic sanity check
            if (store.getWorkspace() == null) {
                throw new IllegalArgumentException("The store has not been assigned a workspace");
            }
    
            if (!store.getWorkspace().equals(workspace)) {
                throw new IllegalArgumentException("Trying to mark as default " + "for workspace "
                        + workspace.getName() + " a store that " + "is contained in "
                        + store.getWorkspace().getName());
            }
        }
        facade.setDefaultDataStore(workspace, store);
    }

    public CoverageStoreInfo getCoverageStore(String id) {
        return (CoverageStoreInfo) getStore(id, CoverageStoreInfo.class);
    }

    public CoverageStoreInfo getCoverageStoreByName(String name) {
        return (CoverageStoreInfo) getStoreByName(name, CoverageStoreInfo.class);
    }
    
    public CoverageStoreInfo getCoverageStoreByName(String workspaceName,
            String name) {
        return getStoreByName(workspaceName,name,CoverageStoreInfo.class);
    }
    
    public CoverageStoreInfo getCoverageStoreByName(WorkspaceInfo workspace,
            String name) {
        return getStoreByName(workspace, name,CoverageStoreInfo.class);
    }
    
    public List<CoverageStoreInfo> getCoverageStoresByWorkspace(
            String workspaceName) {
        return getStoresByWorkspace( workspaceName, CoverageStoreInfo.class );
    }
    
    public List<CoverageStoreInfo> getCoverageStoresByWorkspace(
            WorkspaceInfo workspace) {
        return getStoresByWorkspace( workspace, CoverageStoreInfo.class );
    }

    public List getCoverageStores() {
        return getStores(CoverageStoreInfo.class);
    }

    // Resource methods
    public void add(ResourceInfo resource) {
        if ( resource.getNamespace() == null ) {
            //default to default namespace
            resource.setNamespace( getDefaultNamespace() );
        }
        if ( resource.getNativeName() == null ) {
            resource.setNativeName(resource.getName());
        }
        validate(resource,true);
        
        ResourceInfo added = facade.add(resolve(resource));
        added(added);
    }

    public List<RuntimeException> validate(ResourceInfo resource, boolean isNew) {
        if ( isNull(resource.getName()) ) {
            throw new NullPointerException( "Resource name must not be null");
        }
        if ( isNull(resource.getNativeName())) {
            throw new NullPointerException( "Resource native name must not be null");
        }
        if ( resource.getStore() == null ) {
            throw new IllegalArgumentException( "Resource must be part of a store");
        }
        if ( resource.getNamespace() == null ) {
            throw new IllegalArgumentException( "Resource must be part of a namespace");
        }
        
        StoreInfo store = resource.getStore();
        ResourceInfo existing = getResourceByStore( store, resource.getName(), ResourceInfo.class);
        if ( existing != null && !existing.getId().equals( resource.getId() ) ) {
            String msg = "Resource named '"+resource.getName()+"' already exists in store: '"+ store.getName()+"'";
            throw new IllegalArgumentException( msg );
        }
        
        NamespaceInfo namespace = resource.getNamespace();
        existing =  getResourceByName( namespace, resource.getName(), ResourceInfo.class);
        if ( existing != null && !existing.getId().equals( resource.getId() ) ) {
            String msg = "Resource named '"+resource.getName()+"' already exists in namespace: '"+ namespace.getPrefix()+"'";
            throw new IllegalArgumentException( msg );
        }

        validateKeywords(resource.getKeywords());
        return postValidate(resource, isNew);
    }
    
    public void remove(ResourceInfo resource) {
        //ensure no references to the resource
        if ( !getLayers( resource ).isEmpty() ) {
            throw new IllegalArgumentException( "Unable to delete resource referenced by layer");
        }
        facade.remove(resource);
        removed(resource);
    }

    public void save(ResourceInfo resource) {
        validate(resource,false);
        facade.save(resource);
    }

    public <T extends ResourceInfo> T detach(T resource) {
        return detached(resource, facade.detach(resource));
    }
    
    public <T extends ResourceInfo> T getResource(String id, Class<T> clazz) {
        return facade.getResource(id, clazz);
    }

    public <T extends ResourceInfo> T getResourceByName(String ns, String name, Class<T> clazz) {
        if ("".equals( ns ) ) {
              ns = null;
        }

        if (ns != null) {
            NamespaceInfo namespace = getNamespaceByPrefix(ns);
            if (namespace == null) {
                namespace = getNamespaceByURI(ns); 
            }
            
            if (namespace != null) {
                return getResourceByName(namespace, name, clazz);
            }
            
            return null;
        }

        return getResourceByName((NamespaceInfo) null, name, clazz);

    }
    
    public <T extends ResourceInfo> T getResourceByName(NamespaceInfo ns,
            String name, Class<T> clazz) {
        
        NamespaceInfo namespace = ns;
        if (namespace == null) {
            namespace = getDefaultNamespace();
        }
        T resource = facade.getResourceByName(namespace, name, clazz);
        if (resource == null && ns == null) {
            resource = facade.getResourceByName(CatalogFacade.ANY_NAMESPACE, name, clazz);
        }
        return resource;
    }

    public <T extends ResourceInfo> T getResourceByName(Name name, Class<T> clazz) {
        return getResourceByName( name.getNamespaceURI(), name.getLocalPart(), clazz );
    }
    
    public <T extends ResourceInfo> T getResourceByName( String name, Class<T> clazz ) {
        ResourceInfo resource;
        
        // check is the name is a fully qualified one
        int colon = name.indexOf( ':' );
        if ( colon != -1 ) {
            String ns = name.substring(0, colon);
            String localName = name.substring(colon + 1);
            return getResourceByName(ns, localName, clazz);
        }
        else {
            return getResourceByName((String)null,name,clazz);
        }
    }
    
    public List getResources(Class clazz) {
        return facade.getResources(clazz);
    }

    public List getResourcesByNamespace(NamespaceInfo namespace, Class clazz) {
        return facade.getResourcesByNamespace(namespace, clazz);
    }
    
    public <T extends ResourceInfo> List<T> getResourcesByNamespace(
            String namespace, Class<T> clazz) {
        if ( namespace == null ) {
            return getResourcesByNamespace((NamespaceInfo)null,clazz); 
        }
        
        NamespaceInfo ns = getNamespaceByPrefix(namespace);
        if ( ns == null ) {
            ns = getNamespaceByURI(namespace);
        }
        if ( ns == null ) {
            return Collections.EMPTY_LIST;
        }
        
        return getResourcesByNamespace(ns, clazz);
    }

    public <T extends ResourceInfo> T getResourceByStore(StoreInfo store,
            String name, Class<T> clazz) {
        return facade.getResourceByStore(store, name, clazz);
    }

    public <T extends ResourceInfo> List<T> getResourcesByStore(
            StoreInfo store, Class<T> clazz) {
        return facade.getResourcesByStore(store, clazz); 
    }

    public FeatureTypeInfo getFeatureType(String id) {
        return (FeatureTypeInfo) getResource(id, FeatureTypeInfo.class);
    }

    public FeatureTypeInfo getFeatureTypeByName(String ns, String name) {
        return (FeatureTypeInfo) getResourceByName(ns, name,
                FeatureTypeInfo.class);
    }

    public FeatureTypeInfo getFeatureTypeByName(NamespaceInfo ns, String name) {
        return getResourceByName(ns, name, FeatureTypeInfo.class );
    }

    public FeatureTypeInfo getFeatureTypeByName(Name name) {
        return getResourceByName(name, FeatureTypeInfo.class);
    }
    
    public FeatureTypeInfo getFeatureTypeByName(String name) {
        return (FeatureTypeInfo) getResourceByName(name, FeatureTypeInfo.class);
    }
    
    public List getFeatureTypes() {
        return getResources(FeatureTypeInfo.class);
    }

    public List getFeatureTypesByNamespace(NamespaceInfo namespace) {
        return getResourcesByNamespace(namespace, FeatureTypeInfo.class);
    }
    
    public FeatureTypeInfo getFeatureTypeByStore(DataStoreInfo dataStore,
            String name) {
        return getFeatureTypeByDataStore(dataStore, name);
    }
    
    public FeatureTypeInfo getFeatureTypeByDataStore(DataStoreInfo dataStore,
            String name) {
        return getResourceByStore( dataStore, name, FeatureTypeInfo.class );
    }
    
    public List<FeatureTypeInfo> getFeatureTypesByStore(DataStoreInfo store) {
        return getFeatureTypesByDataStore(store);
    }

    public List<FeatureTypeInfo> getFeatureTypesByDataStore(DataStoreInfo store) {
        return getResourcesByStore(store, FeatureTypeInfo.class);
    }

    public CoverageInfo getCoverage(String id) {
        return (CoverageInfo) getResource(id, CoverageInfo.class);
    }

    public CoverageInfo getCoverageByName(String ns, String name) {
        return (CoverageInfo) getResourceByName(ns, name, CoverageInfo.class);
    }
    
    public CoverageInfo getCoverageByName(NamespaceInfo ns, String name) {
        return (CoverageInfo) getResourceByName(ns, name, CoverageInfo.class);
    }
    
    public CoverageInfo getCoverageByName(Name name) {
        return getResourceByName(name, CoverageInfo.class);
    }
    
    public CoverageInfo getCoverageByName(String name) {
        return (CoverageInfo) getResourceByName( name, CoverageInfo.class );
    }

    public List getCoverages() {
        return getResources(CoverageInfo.class);
    }

    public List getCoveragesByNamespace(NamespaceInfo namespace) {
        return getResourcesByNamespace(namespace, CoverageInfo.class);
    }
    
    public List<CoverageInfo> getCoveragesByStore(CoverageStoreInfo store) {
        return getResourcesByStore(store,CoverageInfo.class);
    }
    
    public CoverageInfo getCoverageByCoverageStore(
            CoverageStoreInfo coverageStore, String name) {
        return getResourceByStore( coverageStore, name, CoverageInfo.class );
    }
    public List<CoverageInfo> getCoveragesByCoverageStore(
            CoverageStoreInfo store) {
        return getResourcesByStore( store, CoverageInfo.class );
    }

    // Layer methods
    public void add(LayerInfo layer) {
        validate(layer,true);
        
        if ( layer.getType() == null ) {
            if ( layer.getResource() instanceof FeatureTypeInfo ) {
                layer.setType( LayerInfo.Type.VECTOR );
            } else if ( layer.getResource() instanceof CoverageInfo ) {
                layer.setType( LayerInfo.Type.RASTER );
            } else if ( layer.getResource() instanceof WMSLayerInfo ) {
                layer.setType( LayerInfo.Type.WMS );
            } else {
                String msg = "Layer type not set and can't be derived from resource";
                throw new IllegalArgumentException( msg );
            }
        }
        
        LayerInfo added = facade.add(resolve(layer));
        added(added);
    }

    public List<RuntimeException> validate( LayerInfo layer, boolean isNew) {
        // TODO: bring back when the layer/publishing split is in act
//        if ( isNull(layer.getName()) ) {
//            throw new NullPointerException( "Layer name must not be null" );
//        }
        
        if ( layer.getResource() == null ) {
            throw new NullPointerException( "Layer resource must not be null" );
        }

        // calling LayerInfo.setName(String) updates the resource (until the layer/publishing split
        // is in act), but that doesn't mean the resource was saved previously, which can leave the
        // catalog in an inconsistent state
        if (null == getResourceByName(layer.getResource().getNamespace(), layer.getResource()
                .getName(), ResourceInfo.class)) {
            throw new IllegalStateException("Found no resource named " + layer.prefixedName()
                    + " , Layer with that name can't be added");
        }
        LayerInfo existing = getLayerByName( layer.getName() );
        if ( existing != null && !existing.getId().equals( layer.getId() ) ) {
            //JD: since layers are not qualified by anything (yet), check 
            // namespace of the resource, if they are different then allow the 
            // layer to be added
            if ( existing.getResource().getNamespace().equals( layer.getResource().getNamespace() ) ) {
                throw new IllegalArgumentException( "Layer named '"+layer.getName()+"' already exists.");
            }
        }
        
        //(JD): not sure if default style should be mandatory
        //if ( layer.getDefaultStyle() == null ){
        //    throw new NullPointerException( "Layer default style must not be null" );
        //}

        return postValidate(layer, isNew);
    }
   
    public void remove(LayerInfo layer) {
        //ensure no references to the layer
        for ( LayerGroupInfo lg : facade.getLayerGroups() ) {
            if ( lg.getLayers().contains( layer ) || layer.equals( lg.getRootLayer() ) ) {
                String msg = "Unable to delete layer referenced by layer group '"+lg.getName()+"'";
                throw new IllegalArgumentException( msg );
            }
        }
        facade.remove(layer);
        removed(layer);
    }

    public void save(LayerInfo layer) {
        validate( layer, false );
        facade.save(layer);
    }
    
    public LayerInfo detach(LayerInfo layer) {
        return detached(layer, facade.detach(layer));
    }

    public LayerInfo getLayer(String id) {
        return facade.getLayer(id);
    }
    
    
    public LayerInfo getLayerByName(Name name) {
        if ( name.getNamespaceURI() != null ) {
            NamespaceInfo ns = getNamespaceByURI( name.getNamespaceURI() );
            if ( ns != null ) {
                return getLayerByName( ns.getPrefix() + ":" + name.getLocalPart() );
            }
        }
        
        return getLayerByName( name.getLocalPart() );
    }
    
    public LayerInfo getLayerByName(String name) {
        LayerInfo result = null;
        int colon = name.indexOf( ':' );
        if ( colon != -1 ) {
            //search by resource name
            String prefix = name.substring( 0, colon );
            String resource = name.substring( colon + 1 );
            
            result = getLayerByName(prefix, resource);
        } else {
            // search in default workspace first
            WorkspaceInfo ws = getDefaultWorkspace();
            if ( ws != null ) {
                result = getLayerByName(ws.getName(), name);
            }
        }

        if(result == null) {
            result = facade.getLayerByName(name);
        }
        
        return result;
    }

    private LayerInfo getLayerByName(String workspace, String resourceName) {
        ResourceInfo r = getResourceByName(workspace, resourceName, ResourceInfo.class);
        if (r == null) {
            return null;
        }
        List<LayerInfo> layers = getLayers(r);
        if (layers.size() == 1) {
            return layers.get(0);
        } else {
            return null;
        }
    }

    public List<LayerInfo> getLayers(ResourceInfo resource) {
        return facade.getLayers(resource);
    }
    
    public List<LayerInfo> getLayers(StyleInfo style) {
        return facade.getLayers(style);
    }
    
    public List<LayerInfo> getLayers() {
        return facade.getLayers();
    }

    // Map methods
    public MapInfo getMap(String id) {
        return facade.getMap(id);
    }

    public MapInfo getMapByName(String name) {
        return facade.getMapByName(name);
    }
    
    public List<MapInfo> getMaps() {
        return facade.getMaps();
    }

    public void add(LayerGroupInfo layerGroup) {
        validate(layerGroup,true);
        resolve(layerGroup);
        
        if ( layerGroup.getStyles().isEmpty() ) {
            for ( PublishedInfo l : layerGroup.getLayers() ) {
                // default style
                layerGroup.getStyles().add(null);
            }
        }
        
        LayerGroupInfo added = facade.add(layerGroup);
        added( added );
    }
    
    public List<RuntimeException> validate( LayerGroupInfo layerGroup, boolean isNew ) {
        if( isNull(layerGroup.getName()) ) {
            throw new NullPointerException( "Layer group name must not be null");
        }

        WorkspaceInfo ws = layerGroup.getWorkspace();
        LayerGroupInfo existing = getLayerGroupByName( ws, layerGroup.getName() );
        if ( existing != null && !existing.getId().equals( layerGroup.getId() ) ) {
            // null workspace can cause layer group in any workspace to be returned, check that
            // workspaces match
            WorkspaceInfo ews = existing.getWorkspace();
            if ((ws == null && ews == null) || (ws != null && ws.equals(ews))) {
                String msg = "Layer group named '" + layerGroup.getName() + "' already exists";
                if (ws != null) {
                    msg += " in workspace " + ws.getName();
                }
                throw new IllegalArgumentException(msg);
            }
        }

        if ( layerGroup.getLayers() == null || layerGroup.getLayers().isEmpty() ) {
            throw new IllegalArgumentException( "Layer group must not be empty");
        }
       
        if ( layerGroup.getStyles() != null && !layerGroup.getStyles().isEmpty() && 
                !(layerGroup.getStyles().size() == layerGroup.getLayers().size()) ) {
            throw new IllegalArgumentException( "Layer group has different number of styles than layers");
        }

        LayerGroupHelper helper = new LayerGroupHelper(layerGroup);
        Stack<LayerGroupInfo> loopPath = helper.checkLoops();
        if (loopPath != null) {
            throw new IllegalArgumentException( "Layer group is in a loop: " + helper.getLoopAsString(loopPath));
        }
        
        // if the layer group has a workspace assigned, ensure that every resource in that layer
        // group lives within the same workspace
        if (ws != null) {
            checkLayerGroupResourceIsInWorkspace(layerGroup, ws);
        }
        
        if (layerGroup.getMode() == null) {
            throw new IllegalArgumentException("Layer group mode must not be null");
        } else if (LayerGroupInfo.Mode.EO.equals(layerGroup.getMode())) {
            if (layerGroup.getRootLayer() == null) {
                throw new IllegalArgumentException("Layer group in mode " + LayerGroupInfo.Mode.EO.getName() + " must have a root layer"); 
            }
            
            if (layerGroup.getRootLayerStyle() == null) {
                throw new IllegalArgumentException("Layer group in mode " + LayerGroupInfo.Mode.EO.getName() + " must have a root layer style");                 
            }
        } else {
            if (layerGroup.getRootLayer() != null) {
                throw new IllegalArgumentException("Layer group in mode " + layerGroup.getMode().getName() + " must not have a root layer"); 
            }
            
            if (layerGroup.getRootLayerStyle() != null) {
                throw new IllegalArgumentException("Layer group in mode " + layerGroup.getMode().getName() + " must not have a root layer style");                 
            }            
        }
        
        return postValidate(layerGroup, isNew);
    }
    
    private void checkLayerGroupResourceIsInWorkspace(LayerGroupInfo layerGroup, WorkspaceInfo ws) {
        if (layerGroup == null) return;
        
        if (layerGroup.getWorkspace() != null && !ws.equals(layerGroup.getWorkspace())) {
            throw new IllegalArgumentException("Layer group within a workspace (" + 
                ws.getName() + ") can not contain resources from other workspace: " + 
                layerGroup.getWorkspace().getName());
        }
        
        checkLayerGroupResourceIsInWorkspace(layerGroup.getRootLayer(), ws);
        checkLayerGroupResourceIsInWorkspace(layerGroup.getRootLayerStyle(), ws);
        if(layerGroup.getLayers() != null) {
            for (PublishedInfo p : layerGroup.getLayers()) {
                if (p instanceof LayerGroupInfo) {
                    checkLayerGroupResourceIsInWorkspace((LayerGroupInfo) p, ws);
                } else {
                    checkLayerGroupResourceIsInWorkspace((LayerInfo) p, ws);                
                }
            }
        }
        
        if (layerGroup.getStyles() != null) {
            for (StyleInfo s : layerGroup.getStyles()) {
                checkLayerGroupResourceIsInWorkspace(s, ws);
            }
        }
    }

    private void checkLayerGroupResourceIsInWorkspace(StyleInfo style, WorkspaceInfo ws) {
        if (style == null) return;
        
        if (style.getWorkspace() != null && !ws.equals(style.getWorkspace())) {
            throw new IllegalArgumentException("Layer group within a workspace (" + 
                ws.getName() + ") can not contain styles from other workspace: " + 
                style.getWorkspace());
        }
    }    
    
    private void checkLayerGroupResourceIsInWorkspace(LayerInfo layer, WorkspaceInfo ws) {
        if (layer == null) return;
        
        ResourceInfo r = layer.getResource();
        if (r.getStore().getWorkspace() != null && !ws.equals(r.getStore().getWorkspace())) {
            throw new IllegalArgumentException("Layer group within a workspace (" + 
                ws.getName() + ") can not contain resources from other workspace: " + 
                r.getStore().getWorkspace().getName());
        }
    }
    
    public void remove(LayerGroupInfo layerGroup) {
        facade.remove(layerGroup);
        removed( layerGroup );
    }
    
    public void save(LayerGroupInfo layerGroup) {
        validate(layerGroup,false);
        facade.save(layerGroup);
    }
    
    public LayerGroupInfo detach(LayerGroupInfo layerGroup) {
        return detached(layerGroup, facade.detach(layerGroup));
    }
    
    public List<LayerGroupInfo> getLayerGroups() {
        return facade.getLayerGroups();
    }

    public List<LayerGroupInfo> getLayerGroupsByWorkspace(String workspaceName) {
        WorkspaceInfo workspace = null;
        if ( workspaceName != null ) {
            workspace = getWorkspaceByName(workspaceName);
            if ( workspace == null ) {
                return Collections.EMPTY_LIST;
            }
        }
        
        return getLayerGroupsByWorkspace(workspace);
    }

    public List<LayerGroupInfo> getLayerGroupsByWorkspace(WorkspaceInfo workspace) {
        return facade.getLayerGroupsByWorkspace(workspace);
    }

    public LayerGroupInfo getLayerGroup(String id) {
        return facade.getLayerGroup(id);
    }
    
    @Override
    public LayerGroupInfo getLayerGroupByName(String name) {

        final LayerGroupInfo layerGroup = getLayerGroupByName((String) null, name);

        if (layerGroup != null)
            return layerGroup;

        // last chance: checking handle prefixed name case
        String workspaceName = null;
        String layerGroupName = null;

        int colon = name.indexOf(':');
        if (colon == -1) {
            layerGroupName = name;
        }
        if (colon != -1) {
            workspaceName = name.substring(0, colon);
            layerGroupName = name.substring(colon + 1);
        }

        return getLayerGroupByName(workspaceName, layerGroupName);
    }

    @Override
    public LayerGroupInfo getLayerGroupByName(String workspaceName, String name) {
        WorkspaceInfo workspace = null;
        if (workspaceName != null) {
            workspace = getWorkspaceByName(workspaceName);
            if(workspace == null){
                return null;
            }
        }

        return getLayerGroupByName(workspace, name);
    }

    @Override
    public LayerGroupInfo getLayerGroupByName(WorkspaceInfo workspace,
            String name) {
        
        if(null == workspace){
            workspace = DefaultCatalogFacade.NO_WORKSPACE;
        }

        LayerGroupInfo layerGroup = facade.getLayerGroupByName(workspace, name);
        return layerGroup;
    }

    public void add(MapInfo map) {
        MapInfo added = facade.add(resolve(map));
        added(added);
    }

    public void remove(MapInfo map) {
        facade.remove(map);
        removed(map);
    }

    public void save(MapInfo map) {
        facade.save(map);
    }
    
    public MapInfo detach(MapInfo map) {
        return detached(map, facade.detach(map));
    }
    
    // Namespace methods
    public NamespaceInfo getNamespace(String id) {
        return facade.getNamespace(id);
    }

    public NamespaceInfo getNamespaceByPrefix(String prefix) {
        if (prefix == null || Catalog.DEFAULT.equals(prefix)) {
            NamespaceInfo ns = getDefaultNamespace();
            if (ns != null) {
                prefix = ns.getPrefix();
            }
        }
        
        return facade.getNamespaceByPrefix(prefix);
    }

    public NamespaceInfo getNamespaceByURI(String uri) {
       return facade.getNamespaceByURI(uri);
    }

    public List getNamespaces() {
        return facade.getNamespaces();
    }

    public void add(NamespaceInfo namespace) {
        validate(namespace,true);
        
        NamespaceInfo added;
        synchronized (facade) {
            added = facade.add(resolve(namespace));
            if ( getDefaultNamespace() == null ) {
                setDefaultNamespace(namespace);
            }
        }
        
        added(added);
    }

    public List<RuntimeException> validate(NamespaceInfo namespace, boolean isNew) {
        if ( isNull(namespace.getPrefix()) ) {
            throw new NullPointerException( "Namespace prefix must not be null");
        }
        
        if(namespace.getPrefix().equals(DEFAULT)) {
            throw new IllegalArgumentException(DEFAULT + " is a reserved keyword, can't be used as the namespace prefix");
        }
        
        NamespaceInfo existing = getNamespaceByPrefix( namespace.getPrefix() );
        if ( existing != null && !existing.getId().equals( namespace.getId() ) ) {
            throw new IllegalArgumentException( "Namespace with prefix '" + namespace.getPrefix() + "' already exists.");
        }
        
        existing = getNamespaceByURI( namespace.getURI() );
        if ( existing != null && !existing.getId().equals( namespace.getId() ) ) {
            throw new IllegalArgumentException( "Namespace with URI '" + namespace.getURI() + "' already exists.");
        }
    
        if ( isNull(namespace.getURI()) ) {
            throw new NullPointerException( "Namespace uri must not be null");
        }
        
        try {
            new URI(namespace.getURI());
        } catch(Exception e) {
            throw new IllegalArgumentException("Invalid URI syntax for '" + namespace.getURI() 
                    + "' in namespace '" + namespace.getPrefix() + "'");
        }

        return postValidate(namespace, isNew);
    }
    
    public void remove(NamespaceInfo namespace) {
        if ( !getResourcesByNamespace(namespace, ResourceInfo.class ).isEmpty() ) {
            throw new IllegalArgumentException( "Unable to delete non-empty namespace.");
        }

        //TODO: remove synchronized block, need transactions
        synchronized(facade) {
            facade.remove(namespace);
         
            NamespaceInfo defaultNamespace = getDefaultNamespace();
            if (namespace.equals(defaultNamespace) || defaultNamespace == null) {
                List<NamespaceInfo> namespaces = facade.getNamespaces(); 
                
                defaultNamespace = null;
                if (!namespaces.isEmpty()) {
                    defaultNamespace = namespaces.get(0);
                }
                
                setDefaultNamespace(defaultNamespace);
                if(defaultNamespace != null) {
                    WorkspaceInfo defaultWorkspace = getWorkspaceByName(defaultNamespace.getPrefix());
                    if(defaultWorkspace != null) {
                        setDefaultWorkspace(defaultWorkspace);
                    }
                }
            }
        }
        removed(namespace);
    }

    public void save(NamespaceInfo namespace) {
        validate(namespace,false);
        
        facade.save(namespace);
    }
    
    public NamespaceInfo detach(NamespaceInfo namespace) {
        return detached(namespace, facade.detach(namespace));
    }

    public NamespaceInfo getDefaultNamespace() {
        return facade.getDefaultNamespace();
    }

    public void setDefaultNamespace(NamespaceInfo defaultNamespace) {
        if(defaultNamespace != null) {
            NamespaceInfo ns = getNamespaceByPrefix( defaultNamespace.getPrefix() );
            if ( ns == null ) {
                throw new IllegalArgumentException( "No such namespace: '" + defaultNamespace.getPrefix() + "'" );
            }
        }
        facade.setDefaultNamespace(defaultNamespace);
    }

    // Workspace methods
    public void add(WorkspaceInfo workspace) {
        validate(workspace,true);
        
        if ( getWorkspaceByName(workspace.getName()) != null ) {
            throw new IllegalArgumentException( "Workspace with name '" + workspace.getName() + "' already exists.");
        }
        
        WorkspaceInfo added;
        synchronized (facade) {
            added = facade.add(resolve(workspace));
            // if there is no default workspace use this one as the default
            if ( getDefaultWorkspace() == null ) {
                setDefaultWorkspace(workspace);
            }
        }
        
        added( added );
    }
    
    public List<RuntimeException> validate(WorkspaceInfo workspace, boolean isNew) {
        if ( isNull(workspace.getName()) ) {
            throw new NullPointerException( "workspace name must not be null");
        }
        
        if(workspace.getName().equals(DEFAULT)) {
            throw new IllegalArgumentException(DEFAULT + " is a reserved keyword, can't be used as the workspace name");
        }
        
        WorkspaceInfo existing = getWorkspaceByName( workspace.getName() );
        if ( existing != null && !existing.getId().equals( workspace.getId() ) ) {
            throw new IllegalArgumentException( "Workspace named '"+ workspace.getName() +"' already exists.");
        }

        return postValidate(workspace, isNew);
    }
    
    public void remove(WorkspaceInfo workspace) {
        //JD: maintain the link between namespace and workspace, remove this when this is no 
        // longer necessary
        if ( getNamespaceByPrefix( workspace.getName() ) != null ) {
            throw new IllegalArgumentException ( "Cannot delete workspace with linked namespace");
        }
        if ( !getStoresByWorkspace( workspace, StoreInfo.class).isEmpty() ) {
            throw new IllegalArgumentException( "Cannot delete non-empty workspace.");
        }
       
        //TODO: remove synchronized block, need transactions
        synchronized(facade) {
            facade.remove(workspace);
         
            WorkspaceInfo defaultWorkspace = getDefaultWorkspace();
            if (workspace.equals(defaultWorkspace) || defaultWorkspace == null) {
                List<WorkspaceInfo> workspaces = facade.getWorkspaces(); 
                
                defaultWorkspace = null;
                if (!workspaces.isEmpty()) {
                    defaultWorkspace = workspaces.get(0);
                }
                
                setDefaultWorkspace(defaultWorkspace);
                if(defaultWorkspace != null) {
                    NamespaceInfo defaultNamespace = getNamespaceByPrefix(defaultWorkspace.getName());
                    if(defaultNamespace != null) {
                        setDefaultNamespace(defaultNamespace);
                    }
                }
            }
        }

        removed( workspace );
    }
    
    public void save(WorkspaceInfo workspace) {
        validate(workspace,false);
        
        facade.save(workspace);
    }
    
    public WorkspaceInfo detach(WorkspaceInfo workspace) {
        return detached(workspace, facade.detach(workspace));
    }
    
    public WorkspaceInfo getDefaultWorkspace() {
        return facade.getDefaultWorkspace();
    }
    
    public void setDefaultWorkspace(WorkspaceInfo workspace) {
        if (workspace != null && facade.getWorkspaceByName(workspace.getName()) == null) {
            facade.add(workspace);
        }
        facade.setDefaultWorkspace(workspace);
    }
    
    public List<WorkspaceInfo> getWorkspaces() {
        return facade.getWorkspaces(); 
    }
    
    public WorkspaceInfo getWorkspace(String id) {
        return facade.getWorkspace(id);
    }
    
    public WorkspaceInfo getWorkspaceByName(String name) {
        if (name == null || Catalog.DEFAULT.equals(name)) {
            WorkspaceInfo ws = getDefaultWorkspace();
            if (ws != null) {
                name = ws.getName();
            }
        }
        return facade.getWorkspaceByName(name);
    }
    
    // Style methods
    public StyleInfo getStyle(String id) {
        return facade.getStyle(id);
    }

    public StyleInfo getStyleByName(String name) {
        return getStyleByName((WorkspaceInfo) null, name);
    }

    public StyleInfo getStyleByName(String workspaceName, String name) {
        if (workspaceName == null) {
            return getStyleByName((WorkspaceInfo)null, name);
        }

        WorkspaceInfo workspace = getWorkspaceByName(workspaceName);
        if (workspace != null) {
            return getStyleByName(workspace, name);
        }
        return null;
    }

    public StyleInfo getStyleByName(WorkspaceInfo workspace, String name) {
        if (workspace == null) {
            workspace = DefaultCatalogFacade.NO_WORKSPACE;
        }
        StyleInfo style = facade.getStyleByName(workspace, name);
        return style;
    }

    public List getStyles() {
        return facade.getStyles();
    }

    public List<StyleInfo> getStylesByWorkspace(String workspaceName) {
        WorkspaceInfo workspace = null;
        if ( workspaceName != null ) {
            workspace = getWorkspaceByName(workspaceName);
            if ( workspace == null ) {
                return Collections.EMPTY_LIST;
            }
        }
        
        return getStylesByWorkspace(workspace);
    }

    public List<StyleInfo> getStylesByWorkspace(WorkspaceInfo workspace) {
        return facade.getStylesByWorkspace(workspace);
    }

    public void add(StyleInfo style) {
        validate(style,true);
        StyleInfo added = facade.add(resolve(style));
        added(added);
    }

    public List<RuntimeException> validate( StyleInfo style, boolean isNew ) {
        if ( isNull(style.getName()) ) {
            throw new NullPointerException( "Style name must not be null");
        }
        if ( isNull(style.getFilename()) ) {
            throw new NullPointerException( "Style fileName must not be null");
        }

        WorkspaceInfo ws = style.getWorkspace();
        StyleInfo existing = getStyleByName( ws, style.getName() );
        if ( existing != null && (isNew || !existing.getId().equals( style.getId() ) )) {
            // null workspace can cause style in any workspace to be returned, check that
            // workspaces match
            WorkspaceInfo ews = existing.getWorkspace();
            String msg =  "Style named '" +  style.getName() +"' already exists";
            if (ews != null) {
                msg += " in workspace " + ews.getName();
            }
            throw new IllegalArgumentException(msg); 
        }

        return postValidate(style, isNew);
    }
    
    public void remove(StyleInfo style) {
        //ensure no references to the style
        for ( LayerInfo l : facade.getLayers() ) {
            if ( style.equals( l.getDefaultStyle() ) || l.getStyles().contains( style )) {
                throw new IllegalArgumentException( "Unable to delete style referenced by '"+ l.getName()+"'");
            }
        }

        for ( LayerGroupInfo lg : facade.getLayerGroups() ) {
            if ( lg.getStyles().contains( style ) || style.equals( lg.getRootLayerStyle() ) ) {
                String msg = "Unable to delete style referenced by layer group '"+lg.getName()+"'";
                throw new IllegalArgumentException( msg );
            }
        }
        
        facade.remove(style);
        removed(style);
    }

    public void save(StyleInfo style) {
        validate(style,false);
        facade.save(style);
    }
    
    public StyleInfo detach(StyleInfo style) {
        return detached(style, facade.detach(style));
    }

    // Event methods
    public Collection getListeners() {
        return Collections.unmodifiableCollection(listeners);
    }

    public void addListener(CatalogListener listener) {
        listeners.add(listener);

    }

    public void removeListener(CatalogListener listener) {
        listeners.remove(listener);
    }
    
    @Override
    public void removeListeners(Class listenerClass) {
        for (Iterator it = listeners.iterator(); it.hasNext();) {
            CatalogListener listener = (CatalogListener) it.next();
            if(listenerClass.isInstance(listener)) {
                it.remove();
            }
        }
    }

    public Iterator search(String cql) {
        // TODO Auto-generated method stub
        return null;
    }

    public ResourcePool getResourcePool() {
        return resourcePool;
    }
    
    public void setResourcePool(ResourcePool resourcePool) {
        this.resourcePool = resourcePool;
    }
    
    public GeoServerResourceLoader getResourceLoader() {
        return resourceLoader;
    }
    public void setResourceLoader(GeoServerResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }
    public void dispose() {
        if ( resourcePool != null ) resourcePool.dispose();
        facade.dispose();
    }
    
    protected void added(CatalogInfo object) {
        fireAdded( object );
    }
    
    protected void removed(CatalogInfo object) {
        fireRemoved( object );
    }
    
    public void fireAdded(CatalogInfo object) {
        CatalogAddEventImpl event = new CatalogAddEventImpl();
        event.setSource(object);

        event(event);
    }
    
    public void fireModified(CatalogInfo object, List propertyNames, List oldValues,
            List newValues) {
        CatalogModifyEventImpl event = new CatalogModifyEventImpl();

        event.setSource(object);
        event.setPropertyNames(propertyNames);
        event.setOldValues(oldValues);
        event.setNewValues(newValues);

        event(event);
    }

    public void firePostModified(CatalogInfo object) {
        CatalogPostModifyEventImpl event = new CatalogPostModifyEventImpl();
        event.setSource( object);
        
        event(event);
    }
    
    public void fireRemoved(CatalogInfo object) {
        CatalogRemoveEventImpl event = new CatalogRemoveEventImpl();
        event.setSource(object);

        event(event);
    }

    protected void event(CatalogEvent event) {
        CatalogException toThrow = null;
        
        for (Iterator l = listeners.iterator(); l.hasNext();) {
            try {
                CatalogListener listener = (CatalogListener) l.next();
                if (event instanceof CatalogAddEvent) {
                    listener.handleAddEvent((CatalogAddEvent) event);
                } else if (event instanceof CatalogRemoveEvent) {
                    listener.handleRemoveEvent((CatalogRemoveEvent) event);
                } else if (event instanceof CatalogModifyEvent) {
                    listener.handleModifyEvent((CatalogModifyEvent) event);
                } else if (event instanceof CatalogPostModifyEvent) {
                    listener.handlePostModifyEvent((CatalogPostModifyEvent)event);
                }
            } catch(Throwable t) {
                if ( t instanceof CatalogException && toThrow == null) {
                    toThrow = (CatalogException) t;
                }
                else {
                    LOGGER.log(Level.WARNING, "Catalog listener threw exception handling event.", t);
                }
            }
        }
        
        if (toThrow != null) {
            throw toThrow;
        }
    }
    
    public static Object unwrap(Object obj) {
        return obj;
    }

    public static void validateKeywords(List<KeywordInfo> keywords) {
        if (keywords != null) {
            for (KeywordInfo kw : keywords) {
                Matcher m = KeywordInfo.RE.matcher(kw.getValue());
                if (!m.matches()) {
                    throw new IllegalArgumentException("Illegal keyword '" + kw + "'. " + 
                        "Keywords must not be empty and must not contain the '\\' character"); 
                }
                if (kw.getVocabulary() != null) {
                    m = KeywordInfo.RE.matcher(kw.getVocabulary());
                    if (!m.matches()) {
                        throw new IllegalArgumentException(
                            "Keyword vocbulary must not contain the '\\' character"); 
                    }
                }
                
            }
        }
    }

    /**
     * Implementation method for resolving all {@link ResolvingProxy} instances.
     */
    public void resolve() {
        facade.setCatalog(this);
        facade.resolve();
        
        if ( listeners == null ) {
            listeners = new ArrayList<CatalogListener>();
        }
        
        if ( resourcePool == null ) {
            resourcePool = ResourcePool.create(this);
        }
    }
    
    protected WorkspaceInfo resolve(WorkspaceInfo workspace) {
        resolveCollections(workspace);
        return workspace;
    }
    
    protected NamespaceInfo resolve(NamespaceInfo namespace) {
        resolveCollections(namespace);
        return namespace;
    }
    
    protected StoreInfo resolve(StoreInfo store) {
        resolveCollections(store);
        
        StoreInfoImpl s = (StoreInfoImpl) store;
        s.setCatalog( this );
        
        return store;
    }

    protected ResourceInfo resolve(ResourceInfo resource) {
        
        ResourceInfoImpl r = (ResourceInfoImpl) resource;
        r.setCatalog(this);
        
        if ( resource instanceof FeatureTypeInfo ) {
            resolve( (FeatureTypeInfo) resource );
        }
        if(r instanceof CoverageInfo){
            resolve((CoverageInfo) resource);
        }
        if(r instanceof WMSLayerInfo){
            resolve((WMSLayerInfo) resource);
        }
        
        return resource;
    }

    private CoverageInfo resolve(CoverageInfo r) {
        CoverageInfoImpl c = (CoverageInfoImpl)r;
        if(c.getDimensions() != null) {
            for (CoverageDimensionInfo dim : c.getDimensions()) {
                if(dim.getNullValues() == null) {
                    ((CoverageDimensionImpl) dim).setNullValues(new ArrayList<Double>());
                }
            }
        }
        resolveCollections(r);
        return r;
    }
    
    /**
     * We don't want the world to be able and call this without 
     * going trough {@link #resolve(ResourceInfo)}
     * @param featureType
     */
    private FeatureTypeInfo resolve(FeatureTypeInfo featureType) {
        FeatureTypeInfoImpl ft = (FeatureTypeInfoImpl) featureType;
        resolveCollections(ft);
        return ft;
    }
    
    private WMSLayerInfo resolve(WMSLayerInfo wmsLayer) {
        WMSLayerInfoImpl impl = (WMSLayerInfoImpl) wmsLayer;
        resolveCollections(impl);
        return wmsLayer;
    }

    protected LayerInfo resolve(LayerInfo layer) {
        if (layer.getAttribution() == null) {
            layer.setAttribution(getFactory().createAttribution());
        }
        resolveCollections(layer);
        return layer;
    }
    
    protected LayerGroupInfo resolve(LayerGroupInfo layerGroup) {
        resolveCollections(layerGroup);
        return layerGroup; 
    }
    
    protected StyleInfo resolve(StyleInfo style) {
        ((StyleInfoImpl)style).setCatalog( this );
        return style;
    }
    
    protected MapInfo resolve(MapInfo map) {
        resolveCollections(map);
        return map;
    }
    
    /**
     * Method which reflectively sets all collections when they are null.
     */
    protected void resolveCollections(Object object) {
        OwsUtils.resolveCollections(object);
    }
    
    protected boolean isNull( String string ) {
        return string == null || "".equals( string.trim() );
    }
    
    <T extends CatalogInfo> T detached(T original, T detached) {
        return detached != null ? detached : original;
    }
    
    protected List<RuntimeException> postValidate(CatalogInfo info, boolean isNew) {
        List<RuntimeException> errors = new ArrayList<RuntimeException>();

        if (!extendedValidation) {
            return errors; 
        }

        for (CatalogValidator constraint : getValidators()) {
            try {
                info.accept(new CatalogValidatorVisitor(constraint, isNew));
            } catch (RuntimeException e) {
                errors.add(e);
            }
        }
        return errors;
    }
    
    static class CatalogValidatorVisitor implements CatalogVisitor {

        CatalogValidator validator;
        boolean isNew;
        
        CatalogValidatorVisitor(CatalogValidator validator, boolean isNew) {
            this.validator = validator;
            this.isNew = isNew;
        }
        public void visit(Catalog catalog) {
        }

        public void visit(WorkspaceInfo workspace) {
            validator.validate(workspace, isNew);
        }

        public void visit(NamespaceInfo namespace) {
            validator.validate(namespace, isNew);
        }

        public void visit(DataStoreInfo dataStore) {
            validator.validate(dataStore, isNew);
        }

        public void visit(CoverageStoreInfo coverageStore) {
            validator.validate(coverageStore, isNew);
        }

        public void visit(WMSStoreInfo wmsStore) {
            validator.validate(wmsStore, isNew);
        }

        public void visit(FeatureTypeInfo featureType) {
            validator.validate(featureType, isNew);
        }

        public void visit(CoverageInfo coverage) {
            validator.validate(coverage, isNew);
        }

        public void visit(LayerInfo layer) {
            validator.validate(layer, isNew);
        }

        public void visit(StyleInfo style) {
            validator.validate(style, isNew);
        }

        public void visit(LayerGroupInfo layerGroup) {
            validator.validate(layerGroup, isNew);
        }

        public void visit(WMSLayerInfo wmsLayer) {
            validator.validate(wmsLayer, isNew);
        }
    }
    
    public void sync( CatalogImpl other ) {
        other.facade.syncTo(facade);
        listeners = other.listeners;
        
        if ( resourcePool != other.resourcePool ) {
            resourcePool.dispose();
            resourcePool = other.resourcePool;
        }
        
        resourceLoader = other.resourceLoader;
    }
    
    public void accept(CatalogVisitor visitor) {
        visitor.visit(this);
    }

    public void resolve(CatalogInfo info) {
        if (info instanceof LayerGroupInfo) {
            resolve((LayerGroupInfo) info);
        } else if (info instanceof LayerInfo) {
            resolve((LayerInfo) info);
        } else if (info instanceof MapInfo) {
            resolve((MapInfo) info);
        } else if (info instanceof NamespaceInfo) {
            resolve((NamespaceInfo) info);
        } else if (info instanceof ResourceInfo) {
            resolve((ResourceInfo) info);
        } else if (info instanceof StoreInfo) {
            resolve((StoreInfo) info);
        } else if (info instanceof StyleInfo) {
            resolve((StyleInfo) info);
        } else if (info instanceof WorkspaceInfo) {
            resolve((WorkspaceInfo) info);
        } else {
            throw new IllegalArgumentException("Unknown resource type: " + info);
        }
    }

    @Override
    public <T extends CatalogInfo> int count(final Class<T> of,
            final Filter filter) {
        final CatalogFacade facade = getFacade();
        return facade.count(of, filter);
    }

    @Override
    public <T extends CatalogInfo> CloseableIterator<T> list(final Class<T> of,
            final Filter filter) {
        return list(of, filter, null, null, null);
    }

    @Override
    public <T extends CatalogInfo> CloseableIterator<T> list(final Class<T> of,
            final Filter filter, Integer offset, Integer count, SortBy sortOrder) {
        CatalogFacade facade = getFacade();
        if (sortOrder != null && !facade.canSort(of, sortOrder.getPropertyName().getPropertyName())) {
            // TODO: use GeoTools' merge-sort code to provide sorting anyways
            throw new UnsupportedOperationException("Catalog backend can't sort on property "
                    + sortOrder.getPropertyName() + " in-process sorting is pending implementation");
        }
        return facade.list(of, filter, offset, count, sortOrder);
    }

    @Override
    public <T extends CatalogInfo> T get(Class<T> type, Filter filter)
            throws IllegalArgumentException {

        final Integer limit = Integer.valueOf(2);
        CloseableIterator<T> it = list(type, filter, null, limit, null);
        T result = null;
        try {
            if (it.hasNext()) {
                result = it.next();
                if (it.hasNext()) {
                    throw new IllegalArgumentException(
                            "Specified query predicate resulted in more than one object");
                }
            }
        } finally {
            it.close();
        }
        return result;
    }
}
